package ch.ethz.fcl.metrobuzz.render.techniques.circos;

import java.util.List;

import javax.vecmath.Vector2d;

import org.lwjgl.opengl.GL11;

import ch.ethz.fcl.metrobuzz.algorithm.sort.SortData;
import ch.ethz.fcl.metrobuzz.algorithm.sort.heapsort.HeapSort;
import ch.ethz.fcl.metrobuzz.render.techniques.bezier.Bezier;
import ch.ethz.fcl.metrobuzz.render.techniques.colors.MBColorTable;
import ch.ethz.fcl.metrobuzz.render.techniques.projection.Projection;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Point2D;
import ch.ethz.fcl.metrobuzz.tools.MBGL;
import ch.ethz.fcl.metrobuzz.tools.MBTools;
import ch.ethz.fcl.metrofont.MetroFont;
import ch.ethz.fcl.metrofont.Pen;

public abstract class CircosDiagram {
	protected Point2D center;
	protected CircosMatrix data;

	protected Point2D[][] outgoingStartPoints;
	protected Point2D[][] outgoingEndPoints;
	protected Point2D[][] incomingStartPoints;
	protected Point2D[][] incomingEndPoints;

	protected float flyoverRingRadius;
	protected float radius;

	protected float arcElementOutRatio = 11.0f / 10;
	protected float arcElementRibbonGapRatio = 30.0f / 31;

	protected float flyoverRingOutRatio = 11.0f / 10;
	protected float flyoverRingRibbonGapRatio = 1.1f;

	protected float statisticBoxInRatio = arcElementOutRatio * 1.05f;
	protected float statisticBoxOutRatio = arcElementOutRatio * 1.1f;

	protected float statisticMarkLongRatio = (arcElementOutRatio - 1) / 2;
	protected float statisticMarkShortRatio = (arcElementOutRatio - 1) / 4;

	protected float renderAngle = (float) Math.PI;

	protected final float minRenderAngle = (float) (Math.PI / (360 * 4));

	protected float gap = (float) (Math.PI / 180 * 30); // 30 degree between two
														// eclipse

	protected float ribbonCtrlPtRatio = 0.25f;

	protected int interline = 16;
	protected Projection proj;

	public CircosDiagram(Point2D center, float radius, CircosMatrix data) {
		data.postProcess();

		this.radius = radius;
		this.flyoverRingRadius = radius * 0.45f;
		this.center = center;
		this.data = data;

		proj = new Projection();
		update();
	}

	public void render() {
		render(null);
	}

	public void render(MetroFont fm) {
		if (data.getTotalSum() == 0)
			return;

		fillHole();
		renderRibbon();

		renderFlyover();
		renderArcElements();

		displayStatistic(fm);
	}

	protected void displayStatistic(MetroFont fm) {
		GL11.glColor4f(0, 0, 0, 1);
		renderInOutRatio();

		if (fm == null)
			return;

		renderMark();
		renderNum(fm);
	}

	public void updateRadius(float radius) {
		this.radius = radius;
		this.flyoverRingRadius = radius * 0.45f;
	}

	protected Point2D[] getEclipsePoints(Point2D start, Point2D end,
			float radius) {
		Vector2d startDir = new Vector2d(start.getX() - center.getX(),
				start.getY() - center.getY());
		Vector2d endDir = new Vector2d(end.getX() - center.getX(), end.getY()
				- center.getY());

		float angle = (float) MBTools.angle(startDir, endDir);

		if (angle < 0)
			angle += Math.PI * 2;

		return getEclipsePoints(radius, startDir, angle);
	}

	protected Point2D[] getEclipsePoints(float radius, Vector2d dir, float angle) {
		float delta = 0;
		if (angle > 0)
			delta = 1 * (float) Math.PI / 360; // 1 degree
		if (angle < 0)
			delta = -1 * (float) Math.PI / 360; // -1 degree
		int size = (int) (angle / delta) + 1;

		Point2D[] points = new Point2D[size + 1];
		for (int i = 0; i < size; i++) {
			points[i] = center.getPointAt(MBTools.rotate(dir, delta * i),
					radius);
		}
		points[size] = center.getPointAt(MBTools.rotate(dir, angle), radius);

		return points;
	}

	protected void renderInOutRatio() {
		if (outgoingStartPoints == null)
			return;
		for (int index = 1; index < data.size(); index++) {
			int[] outOrder = data.getSubRenderOrder()[index];
			Point2D start = outgoingStartPoints[index][outOrder[0]];
			Vector2d dir = new Vector2d(start.getX() - center.getX(),
					start.getY() - center.getY());

			float outAngle = data.getOutgoing()[index]
					/ (data.getTotalSum() - (data.getIncoming()[0] + data
							.getOutgoing()[0])) * renderAngle;
			float inAngle = data.getIncoming()[index]
					/ (data.getTotalSum() - (data.getIncoming()[0] + data
							.getOutgoing()[0])) * renderAngle;

			if (outAngle + inAngle < minRenderAngle)
				continue;

			// render grey for in
			Point2D[] inInnerPoints = getEclipsePoints(radius
					* statisticBoxInRatio, dir, inAngle);
			Point2D[] inOuterPoints = getEclipsePoints(radius
					* statisticBoxOutRatio, dir, inAngle);
			MBColorTable.assignColor(MBColorTable.GREY, 1.0f);
			GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
			for (int i = 0; i < inOuterPoints.length; i++) {
				MBGL.render(inInnerPoints[i]);
				MBGL.render(inOuterPoints[i]);
			}
			GL11.glEnd();

			// render light grey for out
			dir = MBTools.rotate(dir, inAngle);

			Point2D[] outInnerPoints = getEclipsePoints(radius
					* statisticBoxInRatio, dir, outAngle);
			Point2D[] outOuterPoints = getEclipsePoints(radius
					* statisticBoxOutRatio, dir, outAngle);
			MBColorTable.assignColor(MBColorTable.LIGHTGREY, 1.0f);
			GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
			for (int i = 0; i < outInnerPoints.length; i++) {
				MBGL.render(outInnerPoints[i]);
				MBGL.render(outOuterPoints[i]);
			}
			GL11.glEnd();

			GL11.glLineWidth(1.0f);
			MBColorTable.assignColor(MBColorTable.BLACK, 1.0f);
			MBGL.render(inInnerPoints, GL11.GL_LINE_STRIP);
			MBGL.render(inOuterPoints, GL11.GL_LINE_STRIP);
			MBGL.render(outInnerPoints, GL11.GL_LINE_STRIP);
			MBGL.render(outOuterPoints, GL11.GL_LINE_STRIP);

			GL11.glBegin(GL11.GL_LINES);
			MBGL.render(inInnerPoints[0]);
			MBGL.render(inOuterPoints[0]);
			MBGL.render(inInnerPoints[inInnerPoints.length - 1]);
			MBGL.render(inOuterPoints[inInnerPoints.length - 1]);
			MBGL.render(outInnerPoints[outInnerPoints.length - 1]);
			MBGL.render(outOuterPoints[outOuterPoints.length - 1]);

			MBGL.render(inInnerPoints[0]);
			MBGL.render(inOuterPoints[0]);
			GL11.glEnd();
		}
	}

	protected void renderMark() {
		for (int i = 1; i < data.size(); i++) {
			float perc = (data.getIncoming()[i] + data.getOutgoing()[i])
					/ (data.getTotalSum() - (data.getIncoming()[0] + data
							.getOutgoing()[0]));
			float angle = perc * renderAngle;

			int[] outOrder = data.getSubRenderOrder()[i];
			renderMark(outgoingStartPoints[i][outOrder[0]], angle,
					(int) (perc * 100), radius * statisticBoxOutRatio);
		}
	}

	private void renderMark(Point2D start, float angle, int range, float radius) {
		Vector2d startDir = new Vector2d(start.getX() - center.getX(),
				start.getY() - center.getY());

		GL11.glColor4f(0.0f, 0.0f, 0.0f, 0.7f);
		GL11.glLineWidth(1.0f);
		GL11.glBegin(GL11.GL_LINES);
		for (int i = 0; i < range; i++) {
			Vector2d dir = MBTools.rotate(startDir, angle * i / range);
			if (i % 5 == 0)
				renderMark(dir, radius, radius * this.statisticMarkLongRatio);
			else
				renderMark(dir, radius, radius * this.statisticMarkShortRatio);
		}
		GL11.glEnd();
	}

	private void renderMark(Vector2d dir, float radius, float length) {
		Point2D p1 = center.getPointAt(dir, radius);
		Point2D p2 = center.getPointAt(dir, radius + length);

		MBGL.render(p1);
		MBGL.render(p2);
	}

	protected void renderNum(MetroFont fm) {
		if (data.getTotalSum() <= 0)
			return;

		float fontSize = proj.getWorldDistance(15);

		for (int i = 1; i < data.size(); i++) {
			float perc = (data.getIncoming()[i] + data.getOutgoing()[i])
					/ (data.getTotalSum() - (data.getIncoming()[0] + data
							.getOutgoing()[0]));
			float angle = perc * renderAngle;

			int[] outOrder = data.getSubRenderOrder()[i];

			Vector2d startDir = new Vector2d(
					outgoingStartPoints[i][outOrder[0]].getX() - center.getX(),
					outgoingStartPoints[i][outOrder[0]].getY() - center.getY());

			// int range = (int) (perc * 100);
			// for (int j = 0; j <= range; j++) {
			// Vector2d dir = MBTools.rotate(startDir, angle * j / range);
			// if (j % 5 == 0 && range - j > 2) {
			// float fontAngle = (float) MBTools.angle(new Vector2d(1, 0),
			// dir);
			//
			// Point2D point = center.getPointAt(dir, radius
			// * outCircleOutRatio * 1.1f
			// * (1 + (outCircleOutRatio - 1) / 2));
			// Pen pen = new Pen(point.getX(), point.getY(), 0, fontAngle,
			// 0, fontSize);
			// int num = (int) (j / 100.0f * (data.getTotalSum() - (data
			// .getIncoming()[0] + data.getOutgoing()[0])));
			// fm.display(num + "", pen);
			// }
			// }

			// render total number for each arc
			if (angle > minRenderAngle) {
				Vector2d dir = MBTools.rotate(startDir, angle);
				Point2D point = center.getPointAt(dir, radius
						* this.statisticBoxOutRatio
						* (1 + this.statisticMarkLongRatio));
				float fontAngle = (float) MBTools
						.angle(new Vector2d(1, 0), dir);
				Pen pen = new Pen(point.getX(), point.getY(), 0, fontAngle, 0,
						fontSize);
				fm.display(
						(int) (data.getIncoming()[i] + data.getOutgoing()[i])
								+ "", pen);
			}
		}
	}

	public CircosMatrix getData() {
		return data;
	}

	public Point2D getCentroid() {
		return center;
	}

	public float getRadius() {
		return radius;
	}

	public float getFlyoverRingRadius() {
		return this.flyoverRingRadius;
	}

	public float getArcElementOutboundRadius() {
		return radius * this.arcElementOutRatio;
	}

	public void setGlobalMax(float max) {
		renderAngle = (float) (data.getTotalSum() / max * Math.PI);
		update();
	}

	/**
	 * ribbons indicate interchange from one link to another
	 */
	protected void renderRibbon() {
		if (outgoingStartPoints == null || incomingEndPoints == null
				|| outgoingEndPoints == null || incomingStartPoints == null)
			return;

		HeapSort sort = new HeapSort();

		for (int i = 0; i < data.size(); i++)
			for (int j = 0; j < data.size(); j++)
				sort.addData(new SortData(data.getData()[i][j], i * data.size()
						+ j));

		List<SortData> sortedData = sort.getSortedData();

		for (int k = 0; k < sortedData.size(); k++) {
			int index = sortedData.get(k).getIndex();
			int row = index / data.size();
			int col = index % data.size();

			float distribution = data.getData()[row][col];
			if (distribution
					/ ((data.getTotalSum() - data.getOutgoing()[0] - data
							.getIncoming()[0]) - (data.getIncoming()[row] + data
							.getOutgoing()[row])) < minRenderAngle)
				continue;
			renderRibbonHalos(row, col);
			renderRibbon(row, col);
		}
	}

	protected void renderRibbonHalos(int i, int j) {
		int curvePointsSize = 360;

		if (i != j)
			if (i != 0 && j != 0) {
				Point2D[] ctrlPoints1 = {
						outgoingStartPoints[i][j],
						center.getPointAt(outgoingStartPoints[i][j], 0.25f),
						center.getPointAt(incomingEndPoints[i][j],
								0.25f / arcElementRibbonGapRatio),
						incomingEndPoints[i][j] };
				Point2D[] curve1 = Bezier.getBezierSpline(curvePointsSize,
						ctrlPoints1);

				MBColorTable.assignColor(MBColorTable.WHITE, 1.0f);
				GL11.glLineWidth(5.0f);
				GL11.glBegin(GL11.GL_LINE_STRIP);
				for (int k = 0; k < curvePointsSize - 1; k++) {
					// only halo when line is inside the circle
					if (curve1[k].distance(center) < flyoverRingRadius
							* flyoverRingOutRatio
							&& curve1[k + 1].distance(center) < flyoverRingRadius
									* flyoverRingOutRatio)
						MBGL.render(curve1[k]);
				}
				GL11.glEnd();
			}
	}

	protected void renderRibbon(int i, int j) {
		if (i != j)
			if (i != 0 && j != 0) {
				Point2D[] ctrlPoints1 = {
						outgoingStartPoints[i][j],
						center.getPointAt(outgoingStartPoints[i][j],
								ribbonCtrlPtRatio),
						center.getPointAt(incomingEndPoints[i][j],
								ribbonCtrlPtRatio / arcElementRibbonGapRatio),
						incomingEndPoints[i][j] };
				Point2D[] ctrlPoints2 = {
						outgoingEndPoints[i][j],
						center.getPointAt(outgoingEndPoints[i][j],
								ribbonCtrlPtRatio),
						center.getPointAt(incomingStartPoints[i][j],
								ribbonCtrlPtRatio / arcElementRibbonGapRatio),
						incomingStartPoints[i][j] };

				Ribbon.renderAsColorChange(proj, data.getColorIndex()[i],
						data.getColorIndex()[j], ctrlPoints1, ctrlPoints2);
			} else if (i == 0) {
				renderFan(i, j, outgoingStartPoints[i][j],
						outgoingEndPoints[i][j], incomingStartPoints[i][j],
						incomingEndPoints[i][j], true);
			} else if (j == 0) {
				renderFan(i, j, outgoingStartPoints[i][j],
						outgoingEndPoints[i][j], incomingStartPoints[i][j],
						incomingEndPoints[i][j], false);
			}
	}

	private void renderFan(int i, int j, Point2D start1, Point2D end1,
			Point2D start2, Point2D end2, boolean out) {
		Vector2d startVec = new Vector2d(start1.getX() - start2.getX(),
				start1.getY() - start2.getY());
		Vector2d endVec = new Vector2d(end1.getX() - end2.getX(), end1.getY()
				- end2.getY());

		float angle = (float) MBTools.angle(startVec, endVec);
		if (angle < 0)
			angle = (float) (Math.PI * 2 + angle); // make it to anti clockwise

		Point2D[] inEclipse = null;
		Point2D[] outEclipse = null;

		int size = 50;
		for (float k = 0; k < size; k++) {
			if (out) {
				float inRadius = flyoverRingRadius
						* flyoverRingOutRatio
						+ k
						/ size
						* (radius * arcElementRibbonGapRatio - flyoverRingRadius
								* flyoverRingOutRatio);
				float outRadius = inRadius
						+ 1.0f
						/ size
						* (radius * arcElementRibbonGapRatio - flyoverRingRadius
								* flyoverRingOutRatio);
				inEclipse = getEclipsePoints(inRadius,
						MBTools.rotate(startVec, Math.PI), angle);
				outEclipse = getEclipsePoints(outRadius,
						MBTools.rotate(startVec, Math.PI), angle);
			} else {
				float outRadius = flyoverRingRadius
						* flyoverRingOutRatio
						* 1.1f
						+ (size - k)
						/ size
						* (radius - flyoverRingRadius * flyoverRingOutRatio
								* 1.1f);
				float inRadius = outRadius
						- 1.0f
						/ size
						* (radius - flyoverRingRadius * flyoverRingOutRatio
								* 1.1f);
				inEclipse = getEclipsePoints(inRadius, startVec, angle);
				outEclipse = getEclipsePoints(outRadius, startVec, angle);
			}
			MBColorTable.assignGradualColor(data.getColorIndex()[i],
					data.getColorIndex()[j], 1.0f * k / size, 1.0f, 1);
			GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
			for (int l = 0; l < inEclipse.length; l++) {
				GL11.glVertex3f(inEclipse[l].getX(), inEclipse[l].getY(), 0);
				GL11.glVertex3f(outEclipse[l].getX(), outEclipse[l].getY(), 0);
			}
			GL11.glEnd();
		}

		boolean blackOutline = true;
		// only width large enough display the outline as black
		if (data.getData()[i][j] / data.getTotalSum() * renderAngle < minRenderAngle
				* interline) {
			blackOutline = false;
		}

		GL11.glLineWidth(1.0f);

		// render out bounds
		if (out) {
			outEclipse = getEclipsePoints(radius * arcElementRibbonGapRatio,
					MBTools.rotate(startVec, Math.PI), angle);
			GL11.glBegin(GL11.GL_LINE_STRIP);
			for (int l = 0; l < outEclipse.length; l++) {
				if (blackOutline)
					MBColorTable.assignColor(MBColorTable.BLACK, 0.5f);
				else
					MBColorTable.assignGradualColor(data.getColorIndex()[i],
							data.getColorIndex()[j], 1.0f * l
									/ outEclipse.length, 1.0f, 2);
				GL11.glVertex3f(outEclipse[l].getX(), outEclipse[l].getY(), 0);
			}
			GL11.glEnd();
		}
		Point2D[] ctrlLine1 = { start1, start2 };
		Point2D[] startLine = Bezier.getBezierSpline(size, ctrlLine1);
		Point2D[] ctrlLine2 = { end1, end2 };
		Point2D[] endLine = Bezier.getBezierSpline(size, ctrlLine2);

		// render one side black line
		GL11.glBegin(GL11.GL_LINE_STRIP);
		for (int k = 0; k <= size; k++) {
			if (blackOutline)
				MBColorTable.assignColor(MBColorTable.BLACK, 0.5f);
			else
				MBColorTable.assignGradualColor(data.getColorIndex()[i],
						data.getColorIndex()[j], 1.0f * k / size, 1.0f, 2);
			MBGL.render(startLine[k]);
		}
		GL11.glEnd();

		// render another side black line
		GL11.glBegin(GL11.GL_LINE_STRIP);
		for (int k = 0; k <= size; k++) {
			if (blackOutline)
				MBColorTable.assignColor(MBColorTable.BLACK, 0.5f);
			else
				MBColorTable.assignGradualColor(data.getColorIndex()[i],
						data.getColorIndex()[j], 1.0f * k / size, 1.0f, 2);
			MBGL.render(endLine[k]);
		}
		GL11.glEnd();
	}

	protected void renderFlyover() {
		Vector2d y = new Vector2d(0, 1);

		int size = 100;

		Point2D in;
		Point2D out;

		// // render inner circle halos
		// MBColorTable.assignColor(MBColorTable.WHITE, 1.0f);
		// GL11.glLineWidth(3.0f);
		// GL11.glBegin(GL11.GL_LINE_STRIP);
		// for (int i = 0; i < size; i++) {
		// in = center.getPointAt(MBTools.rotate(y, Math.PI * 2 / size * i),
		// innerRadius);
		// MBGL.render(in);
		// }
		// in = center.getPointAt(y, innerRadius);
		// MBGL.render(in);
		// GL11.glEnd();

		MBColorTable.assignColor(MBColorTable.LIGHTGREY, 1.0f);
		GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
		for (int i = 0; i < size; i++) {
			in = center.getPointAt(MBTools.rotate(y, Math.PI * 2 / size * i),
					flyoverRingRadius);
			out = center.getPointAt(MBTools.rotate(y, Math.PI * 2 / size * i),
					flyoverRingRadius * flyoverRingOutRatio);

			GL11.glVertex3f(in.getX(), in.getY(), 0);
			GL11.glVertex3f(out.getX(), out.getY(), 0);
		}

		in = center.getPointAt(y, flyoverRingRadius);
		out = center.getPointAt(y, flyoverRingRadius * flyoverRingOutRatio);

		GL11.glVertex3f(in.getX(), in.getY(), 0);
		GL11.glVertex3f(out.getX(), out.getY(), 0);
		GL11.glEnd();

		// render inner circle bounds
		MBColorTable.assignColor(MBColorTable.BLACK, 0.5f);
		GL11.glLineWidth(1.0f);
		GL11.glBegin(GL11.GL_LINE_STRIP);
		for (int i = 0; i < size; i++) {
			in = center.getPointAt(MBTools.rotate(y, Math.PI * 2 / size * i),
					flyoverRingRadius);
			MBGL.render(in);
		}
		in = center.getPointAt(y, flyoverRingRadius);
		MBGL.render(in);
		GL11.glEnd();

		// render outer circle bounds
		GL11.glBegin(GL11.GL_LINE_STRIP);
		for (int i = 0; i < size; i++) {
			out = center.getPointAt(MBTools.rotate(y, Math.PI * 2 / size * i),
					flyoverRingRadius * flyoverRingOutRatio);
			MBGL.render(out);
		}
		out = center.getPointAt(y, flyoverRingRadius * flyoverRingOutRatio);
		MBGL.render(out);
		GL11.glEnd();

		GL11.glLineWidth(2.0f);
		MBColorTable.assignColor(data.getColorIndex()[0], 1.0f);
		for (int i = 1; i < data.size(); i++) {
			Point2D[] points = getEclipsePoints(outgoingStartPoints[0][i],
					outgoingEndPoints[0][i], flyoverRingRadius
							* flyoverRingOutRatio);
			MBGL.render(points, GL11.GL_LINE_STRIP);
		}
	}

	protected void renderArcElement(int index, Point2D start, float radius,
			float angle) {
		if (angle < minRenderAngle)
			return;

		Vector2d dir = new Vector2d(start.getX() - center.getX(), start.getY()
				- center.getY());
		Point2D[] innerPoints = getEclipsePoints(radius, dir, angle);
		Point2D[] outerPoints = getEclipsePoints(radius * arcElementOutRatio,
				dir, angle);

		GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
		for (int i = 0; i < innerPoints.length; i++) {
			GL11.glVertex3f(innerPoints[i].getX(), innerPoints[i].getY(), 0);

			GL11.glVertex3f(outerPoints[i].getX(), outerPoints[i].getY(), 0);
		}
		GL11.glEnd();

		GL11.glLineWidth(1.0f);
		GL11.glColor4f(0.0f, 0.0f, 0.0f, 0.5f);
		GL11.glBegin(GL11.GL_LINE_STRIP);
		MBGL.render(innerPoints[innerPoints.length - 1]);
		for (int i = innerPoints.length - 1; i >= 0; i--)
			MBGL.render(outerPoints[i]);
		GL11.glVertex3f(innerPoints[0].getX(), innerPoints[0].getY(), 0);
		GL11.glEnd();
	}

	public float getRenderAngle() {
		return renderAngle;
	}

	protected abstract void fillHole();

	protected abstract void renderArcElements();

	public void updateCenter(Point2D center) {
		this.center = center;
		update();
	}

	public void updateRaidus(float radius) {
		this.radius = radius;
		update();
	}

	public abstract void update();
}
